﻿/*
Copyright (C) 2009  Tomasz Chrzanowski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data.SqlClient;
using System.Drawing;
using System.IO;
using System.ServiceProcess;
using System.Windows.Forms;
using Com.Tom.CdcCdcCommon;


namespace Com.TomCdc.ChangeDataCapture
{
    public partial class MainForm : Form
    {
        /// <summary>
        /// Create a logger for use in this class.
        /// </summary>
        private static readonly log4net.ILog log =
            log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private enum UiContextState
        {
            ConnectionNotDefined,
            ConnectionDefined,
            TablesLoading,
            TablesLoaded,
            SchemaToInstall,
            SchemaInstalling,
            SchemaInstalled
        }

        private enum MainStatus
        {
            Ready,
            Error,
            Cancelled,
            Processing
        }

        private const string ICON_MAIN_STATUS_READY = "StatusReady.ico";
        private const string ICON_MAIN_STATUS_FAILURE = "StatusFailure.ico";
        private const string ICON_AGENT_SERVICE_WAIT = "AgentServiceStatusWait.ico";
        private const string ICON_AGENT_SERVICE_RUN = "AgentServiceStatusRun.ico";
        private const string ICON_AGENT_SERVICE_PAUSE = "AgentServiceStatusPause.ico";
        private const string ICON_AGENT_SERVICE_STOP = "AgentServiceStatusStop.ico";
        private const string ICON_AGENT_SERVICE_ERROR = "AgentServiceStatusError.ico";

        /// <summary>
        /// The time to wait for service state change.
        /// </summary>
        private readonly TimeSpan AGENT_SERVICE_WAIT_TIME = TimeSpan.FromSeconds(15);

        private SqlConnectionStringBuilder _trackDbConnStr;
        private SqlConnectionStringBuilder _stageDbConnStr;
        private SqlConnectionStringBuilder _storeDbConnStr;
        private List<TreeNodeTable> _sourceDbTables;
        private BackgroundWorker _installSchemaBkgWorker;
        private BackgroundWorker _refreshTableListBkgWorker;
        private BackgroundWorker _checkAgentServiceStatusBkgWorker;
        private ServiceController _agentServiceController;


        public MainForm()
        {
            InitializeComponent();
            this.uxMainSplitContainer.Panel2MinSize =
                uxMainSplitContainer.Width - uxMainSplitContainer.Panel1MinSize - uxMainSplitContainer.SplitterWidth;
            uxTrackedConnInfo.Text = "";
            uxStagingConnInfo.Text = "";
            uxStorageConnInfo.Text = "";
            uxAgentServiceInfo.Text = "";
            uxInstProgInfoMainStep.Text = "";
            uxInstProgInfoSubStep.Text = "";
            uxProgressCircle.StylePreset = MRG.Controls.UI.LoadingCircle.StylePresets.Custom;
            EnableDisableUIControls(UiContextState.ConnectionNotDefined);
            SetMainStatusInfo(MainStatus.Ready, "", "");
        }

        private void uxConnectTrackedDb_Click(object sender, EventArgs e)
        {
            if (IsSchemaToInstall() &&
                DialogResult.No == MessageBox.Show("By changing connection to tracked database, " +
                "you will discard all not installed changes. Continue?", "",
                MessageBoxButtons.YesNo, MessageBoxIcon.Question))
            {
                return;
            }

            DatabaseConnection dbConnDialog = new DatabaseConnection();
            if (dbConnDialog.ShowDialog() != DialogResult.OK) return;

            bool dbOk = false;
            CdcDatabaseType dbType = CommonHelper.IdentifyDatabaseType(dbConnDialog.TestedConnection.ConnectionString);

            if (dbType == CdcDatabaseType.Tracked)
            {
                dbOk = true;
            }
            else if (dbType == CdcDatabaseType.Undefined)
            {
                DialogResult res = MessageBox.Show("Selected database has not been recognized as tracked database." +
                    Environment.NewLine + Environment.NewLine + "Would you like to execute setup script?",
                    "", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

                if (res == DialogResult.Yes)
                {
                    CdcDatabaseSetup dbSetup = new CdcDatabaseSetup(dbConnDialog.TestedConnection.ConnectionString);
                    try
                    {
                        dbSetup.Setup(CdcDatabaseType.Tracked);
                        MessageBox.Show("Setup script executed successfully.", "",
                            MessageBoxButtons.OK, MessageBoxIcon.Information);
                        dbOk = true;
                    }
                    catch (Exception ex)
                    {
                        uxTrackedConnInfo.Text = "";
                        _trackDbConnStr = null;
                        SetMainStatusInfo(MainStatus.Error, "Installation failed.", ex.Message);
                        return;
                    }
                }
            }
            else
            {
                MessageBox.Show("Selected database has been identified as " + dbType + ", it can't be used as tracked.",
                    "Not tracked database", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }


            if (dbOk)
            {
                uxTrackedTablesTree.Nodes.Clear();
                uxInstallationList.Items.Clear();
                uxTrackedConnInfo.Text = dbConnDialog.TestedConnection.DataSource + "." +
                    dbConnDialog.TestedConnection.InitialCatalog;
                _trackDbConnStr = dbConnDialog.TestedConnection;
                EnableDisableUIControls(UiContextState.ConnectionDefined);
                SetMainStatusInfo(MainStatus.Ready, "", "");
            }
            else
            {
                uxTrackedConnInfo.Text = "";
                _trackDbConnStr = null;
                SetMainStatusInfo(MainStatus.Error, "Not tracked database.", "Selected database has not been " +
                    "recognized as tracked database.");
            }

        }

        private void uxConnectStagingDb_Click(object sender, EventArgs e)
        {
            DatabaseConnection dbConnDialog = new DatabaseConnection();

            if (dbConnDialog.ShowDialog() == DialogResult.OK)
            {
                bool dbOk = false;
                CdcDatabaseType dbType = CommonHelper.IdentifyDatabaseType(dbConnDialog.TestedConnection.ConnectionString);

                if (dbType == CdcDatabaseType.Staging)
                {
                    dbOk = true;
                }
                else if (dbType == CdcDatabaseType.Undefined)
                {
                    DialogResult res = MessageBox.Show("Selected database has not been recognized as staging database." +
                        Environment.NewLine + Environment.NewLine + "Would you like to execute setup script?",
                        "", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

                    if (res == DialogResult.Yes)
                    {
                        CdcDatabaseSetup dbSetup = new CdcDatabaseSetup(dbConnDialog.TestedConnection.ConnectionString);
                        try
                        {
                            dbSetup.Setup(CdcDatabaseType.Staging);
                            MessageBox.Show("Setup script executed successfully.", "",
                                MessageBoxButtons.OK, MessageBoxIcon.Information);
                            dbOk = true;
                        }
                        catch (Exception ex)
                        {
                            uxStagingConnInfo.Text = "";
                            _stageDbConnStr = null;
                            SetMainStatusInfo(MainStatus.Error, "Installation failed.", ex.Message);
                            return;
                        }
                    }
                }
                else
                {
                    MessageBox.Show("Selected database has been identified as " + dbType + ", it can't be used as staging.",
                        "Not staging database", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                }

                if (dbOk)
                {
                    uxStagingConnInfo.Text = dbConnDialog.TestedConnection.DataSource + "." +
                        dbConnDialog.TestedConnection.InitialCatalog;
                    _stageDbConnStr = dbConnDialog.TestedConnection;
                    SetMainStatusInfo(MainStatus.Ready, "", "");
                }
                else
                {
                    uxStagingConnInfo.Text = "";
                    _stageDbConnStr = null;
                    SetMainStatusInfo(MainStatus.Error, "Not staging database.", "Selected database has not been " +
                        "recognized as staging database.");
                };
            }
        }

        private void uxStorageDb_Click(object sender, EventArgs e)
        {
            DatabaseConnection dbConnDialog = new DatabaseConnection();

            if (dbConnDialog.ShowDialog() == DialogResult.OK)
            {
                bool dbOk = false;
                CdcDatabaseType dbType = CommonHelper.IdentifyDatabaseType(dbConnDialog.TestedConnection.ConnectionString);

                if (dbType == CdcDatabaseType.Storage)
                {
                    dbOk = true;
                }
                else if (dbType == CdcDatabaseType.Undefined)
                {
                    DialogResult res = MessageBox.Show("Selected database has not been recognized as storage database." +
                        Environment.NewLine + Environment.NewLine + "Would you like to execute setup script?",
                        "", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

                    if (res == DialogResult.Yes)
                    {
                        CdcDatabaseSetup dbSetup = new CdcDatabaseSetup(dbConnDialog.TestedConnection.ConnectionString);
                        try
                        {
                            dbSetup.Setup(CdcDatabaseType.Storage);
                            MessageBox.Show("Setup script executed successfully.", "",
                                MessageBoxButtons.OK, MessageBoxIcon.Information);
                            dbOk = true;
                        }
                        catch (Exception ex)
                        {
                            uxStorageConnInfo.Text = "";
                            _storeDbConnStr = null;
                            SetMainStatusInfo(MainStatus.Error, "Installation failed.", ex.Message);
                        }
                    }
                }
                else
                {
                    MessageBox.Show("Selected database has been identified as " + dbType + ", it can't be used as storage.",
                        "Not storage database", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                }

                if (dbOk)
                {
                    uxStorageConnInfo.Text = dbConnDialog.TestedConnection.DataSource + "." +
                        dbConnDialog.TestedConnection.InitialCatalog;
                    _storeDbConnStr = dbConnDialog.TestedConnection;
                    SetMainStatusInfo(MainStatus.Ready, "", "");
                }
                else
                {
                    uxStorageConnInfo.Text = "";
                    _storeDbConnStr = null;
                    SetMainStatusInfo(MainStatus.Error, "Not storage database.", "Selected database has not been " +
                        "recognized as storage database.");
                }
            }
        }

        private void uxRefresh_Click(object sender, EventArgs e)
        {
            if (_trackDbConnStr != null)
            {
                if (IsSchemaToInstall() &&
                     DialogResult.No == MessageBox.Show("By refreshing tables list, " +
                     "you will discard all not installed changes. Continue?", "",
                     MessageBoxButtons.YesNo, MessageBoxIcon.Question))
                {
                    return;
                }
                else
                {
                    RefreshTablesList();
                }
            }
            else
            {
                MessageBox.Show("Connect to tracked database first.", "Refresh error",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }

        }


        private void RefreshTablesList()
        {
            EnableDisableUIControls(UiContextState.TablesLoading);
            SetMainStatusInfo(MainStatus.Processing, "Loading tables list.", "");

            _refreshTableListBkgWorker = new BackgroundWorker();
            _refreshTableListBkgWorker.DoWork += RefreshTables_DoWork;
            _refreshTableListBkgWorker.ProgressChanged += RefreshTables_ProgressChanged;
            _refreshTableListBkgWorker.RunWorkerCompleted += RefreshTables_RunWorkerCompleted;
            _refreshTableListBkgWorker.WorkerReportsProgress = true;
            _refreshTableListBkgWorker.WorkerSupportsCancellation = true;


            uxInstallationList.Items.Clear();
            uxTrackedTablesTree.Nodes.Clear();
            uxTrackedTablesTree.CheckBoxes = false;
            uxTrackedTablesTree.ShowLines = false;
            uxTrackedTablesTree.Nodes.Add(new TreeNode("Loading tables ..."));

            _refreshTableListBkgWorker.RunWorkerAsync(_storeDbConnStr);
        }

        private void RefreshTables_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                uxTrackedTablesTree.Nodes.Clear();
                EnableDisableUIControls(UiContextState.ConnectionDefined);
                SetMainStatusInfo(MainStatus.Error, "",
                    e.Error.Message + Environment.NewLine +
                    (e.Error.InnerException != null ? e.Error.InnerException.Message : ""));

            }
            else if (e.Cancelled)
            {
                uxTrackedTablesTree.Nodes.Clear();
                EnableDisableUIControls(UiContextState.ConnectionDefined);
                SetMainStatusInfo(MainStatus.Cancelled, "", "");
            }
            else
            {
                uxTrackedTablesTree.Nodes.Clear();
                uxTrackedTablesTree.CheckBoxes = true;
                uxTrackedTablesTree.ShowLines = true;

                bool installRequired = false;
                foreach (TreeNodeTable tblNode in _sourceDbTables)
                {
                    tblNode.SetInstallRequiredFlag();
                    if (tblNode.IsInstallRequired)
                    {
                        installRequired = true;
                    }
                    uxTrackedTablesTree.Nodes.Add(tblNode);
                }

                if (installRequired)
                {
                    EnableDisableUIControls(UiContextState.SchemaToInstall);
                }
                else
                {
                    EnableDisableUIControls(UiContextState.TablesLoaded);
                }

                SetMainStatusInfo(MainStatus.Ready, "", "");
            }

        }

        private void RefreshTables_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            SetMainStatusInfo(MainStatus.Processing, (string)e.UserState, "");
        }

        private void RefreshTables_DoWork(object sender, DoWorkEventArgs e)
        {
            _refreshTableListBkgWorker.ReportProgress(0, "Retrieving tables list.");

            SourceSchemaFactory schemaFactory = new SourceSchemaFactory(_trackDbConnStr.ConnectionString);

            if (_refreshTableListBkgWorker.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

            schemaFactory.InitializeFactory();

            if (_refreshTableListBkgWorker.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

            SourceTable[] allSrcTables = schemaFactory.CreateAllSourceTables(false);

            _refreshTableListBkgWorker.ReportProgress(60, "Building tables list.");

            if (_refreshTableListBkgWorker.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

            _sourceDbTables = new List<TreeNodeTable>(allSrcTables.Length);

            foreach (SourceTable srcTbl in allSrcTables)
            {
                if (_refreshTableListBkgWorker.CancellationPending)
                {
                    e.Cancel = true;
                    return;
                }

                TreeNodeTable nodeTbl = new TreeNodeTable(srcTbl);

                foreach (SourceColumn srcCol in srcTbl.Columns.Values)
                {
                    TreeNodeColumn newColNode = new TreeNodeColumn(srcCol);
                    nodeTbl.Nodes.Add(newColNode);
                }

                nodeTbl.TableAddedStateChanged += new TreeNodeTable.TableStateChangedHandler(AddTableToInstallList);
                nodeTbl.TableRemovedStateChanged +=
                    new TreeNodeTable.TableStateChangedHandler(RemoveTableFromInstallList);

                nodeTbl.SetNodeCheckedState();
                nodeTbl.SetNodeIcon();
                _sourceDbTables.Add(nodeTbl);
            }

            _refreshTableListBkgWorker.ReportProgress(100, "Ready.");
        }

        private void EnableDisableUIControls(UiContextState newUiState)
        {

            if (newUiState == UiContextState.ConnectionDefined)
            {
                uxRefresh.Enabled = true;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = true;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = false;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = true;
            }

            if (newUiState == UiContextState.TablesLoading)
            {
                uxRefresh.Enabled = false;
                uxRefreshStop.Enabled = true;

                uxConnectTrackedDb.Enabled = false;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = false;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = false;
            }

            if (newUiState == UiContextState.TablesLoaded)
            {
                uxRefresh.Enabled = true;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = true;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = false;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = true;
                uxInstallationProgressBar.Value = 0;
            }

            if (newUiState == UiContextState.ConnectionNotDefined)
            {
                uxRefresh.Enabled = false;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = true;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = false;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = false;
            }

            if (newUiState == UiContextState.SchemaToInstall)
            {
                uxRefresh.Enabled = true;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = true;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = true;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = true;
            }

            if (newUiState == UiContextState.SchemaInstalling)
            {
                uxRefresh.Enabled = false;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = false;
                uxConnectStagingDb.Enabled = false;
                uxConnectStorageDb.Enabled = false;
                uxConnectServiceServer.Enabled = false;

                uxInstall.Enabled = false;
                uxInstallStop.Enabled = true;

                uxTrackedTablesTree.Enabled = false;
            }

            if (newUiState == UiContextState.SchemaInstalled)
            {
                uxRefresh.Enabled = true;
                uxRefreshStop.Enabled = false;

                uxConnectTrackedDb.Enabled = true;
                uxConnectStagingDb.Enabled = true;
                uxConnectStorageDb.Enabled = true;
                uxConnectServiceServer.Enabled = true;

                uxInstall.Enabled = true;
                uxInstallStop.Enabled = false;

                uxTrackedTablesTree.Enabled = false;
            }
        }

        private void uxInstall_Click(object sender, EventArgs e)
        {
            string validationErrMsg;
            if (IsReadyForSchemaInstallation(out validationErrMsg))
            {
                InstallSchema();
            }
            else
            {
                MessageBox.Show(validationErrMsg, "Installation warning.",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }



        private void InstallSchema()
        {
            EnableDisableUIControls(UiContextState.SchemaInstalling);
            SetMainStatusInfo(MainStatus.Processing, "", "");

            _installSchemaBkgWorker = new BackgroundWorker();
            _installSchemaBkgWorker.WorkerReportsProgress = true;
            _installSchemaBkgWorker.WorkerSupportsCancellation = true;

            _installSchemaBkgWorker.DoWork += InstallSchema_DoWork;
            _installSchemaBkgWorker.ProgressChanged += InstallSchema_ProgressChanged;
            _installSchemaBkgWorker.RunWorkerCompleted += InstallSchema_RunWorkerCompleted;

            List<ListViewInstallItem> itemsToInstall = new List<ListViewInstallItem>();
            foreach (ListViewInstallItem item in uxInstallationList.Items)
            {
                if (item.InstallStatus != InstallStepStatus.Success)
                {
                    itemsToInstall.Add(item);
                }
            }

            _installSchemaBkgWorker.RunWorkerAsync(itemsToInstall.ToArray());
        }

        void InstallSchema_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                log.Error("Schema installation error.", e.Error);                
                SetMainStatusInfo(MainStatus.Error, "", e.Error.Message);
            }
            else if (e.Cancelled)
            {
                SetMainStatusInfo(MainStatus.Cancelled, "", "");
            }
            else
            {
                SetMainStatusInfo(MainStatus.Ready, "", "");
            }
            uxInstProgInfoMainStep.Text = "";
            uxInstProgInfoSubStep.Text = "";
            EnableDisableUIControls(UiContextState.SchemaInstalled);
        }

        void InstallSchema_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            uxInstallationProgressBar.Value = e.ProgressPercentage;

            AsyncWorkProgressState progressState = e.UserState as AsyncWorkProgressState;

            if (progressState != null && progressState.StateValue.Length > 0)
            {
                switch (progressState.Type)
                {
                    case AsyncWorkProgressType.ProgressDescription:
                        if (progressState.StateValue[0] is string)
                        {
                            uxInstProgInfoSubStep.Text = (string)progressState.StateValue[0];
                        }
                        break;
                    case AsyncWorkProgressType.ServiceStatus:
                        uxAgentServiceStatusIcon.Image =
                            (progressState.StateValue[0] as Image);
                        break;
                    case AsyncWorkProgressType.StepItemStatus:
                        ListViewInstallItem item = progressState.StateValue[0] as ListViewInstallItem;
                        if (item != null)
                        {
                            item.SetStepUiStatus();
                            uxInstallationList.EnsureVisible(item.Index);
                            uxInstProgInfoMainStep.Text = item.TableIdentifier;
                            SetMainStatusInfo(MainStatus.Processing, "Processing: " + item.TableIdentifier, "");
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Performs installation of sql schemas for tables (each wrapped in <see cref="ListViewInstallItem"/>)
        /// passed as <see cref="DoWorkEventArgs.Argument"/> argument. Three main steps are: stop the agent service,
        /// perform installation, restore initial service state. Method supports worker cancellation.
        /// </summary>
        private void InstallSchema_DoWork(object sender, DoWorkEventArgs e)
        {
            // converts worker argument to list of items to install a schema
            ListViewInstallItem[] itemsToInstall = e.Argument as ListViewInstallItem[];

            // input argument validation
            if (itemsToInstall == null)
            {
                throw new ArgumentException("The DoWorkEventArgs.Argument object is null or has a wrong type.");
            }

            // checks if worker cancelled
            if (_installSchemaBkgWorker.CancellationPending)
            {
                return;
            }

            // stores status of a service before installation, after installation state will be restored
            ServiceControllerStatus initialServiceStatus;
            try
            {
                // gets current service status
                _agentServiceController.Refresh();

                // remembers initial status
                initialServiceStatus = _agentServiceController.Status;

                // takes action based on current service state
                if (initialServiceStatus == ServiceControllerStatus.Paused ||
                    initialServiceStatus == ServiceControllerStatus.Running)
                {
                    // service not stopped, can be stopped
                    _agentServiceController.Stop();
                    _agentServiceController.WaitForStatus(ServiceControllerStatus.Stopped, AGENT_SERVICE_WAIT_TIME);
                }
                else if (initialServiceStatus == ServiceControllerStatus.Stopped)
                {
                    // service already stopped
                }
                else
                {
                    // service not stopped, cannot be stopped
                    throw new InvalidOperationException("The agent service is in not operable state.");
                }

            }
            catch (Exception ex)
            {
                // stops installation, cannot do operations on the service
                throw new NotOperableAgentServiceException("The agent service cannot be turned into the state which" +
                    " guarantees safe schema installation. Cannot connect or transitional state.", ex);
            }

            // when it gets to this point the agent service is stopped successfully
            _installSchemaBkgWorker.ReportProgress(0, new AsyncWorkProgressState(
                AsyncWorkProgressType.ServiceStatus,
                new object[] { GetAgentServiceIcon(ServiceControllerStatus.Stopped) })
                );

            try
            {
                // checks if worker cancelled
                if (_installSchemaBkgWorker.CancellationPending)
                {
                    return;
                }

                // creates schema installer object
                CdcSchemaInstaller schemaInstaller = new CdcSchemaInstaller(
                    _trackDbConnStr.ConnectionString, _stageDbConnStr.ConnectionString,
                    _storeDbConnStr.ConnectionString, _installSchemaBkgWorker);

                // checks if worker cancelled
                if (_installSchemaBkgWorker.CancellationPending)
                {
                    return;
                }

                // goes through all items passed to the worker and performs installation, one by one
                foreach (ListViewInstallItem itemToInstall in itemsToInstall)
                {
                    try
                    {
                        // sets item status to "running" and updates UI
                        itemToInstall.InstallStatus = InstallStepStatus.Running;
                        _installSchemaBkgWorker.ReportProgress(0, new AsyncWorkProgressState(
                            AsyncWorkProgressType.StepItemStatus, new object[] { itemToInstall }));


                        // checks if any column marked for tracking, if not uninstall needed
                        if (itemToInstall.TrackedTable.IsDropTrackingRequested())
                        {
                            schemaInstaller.UninstallTableTracking(
                                itemToInstall.TrackedTable.TableSchema,
                                itemToInstall.TrackedTable.TableIdentifier);
                        }
                        else
                        {
                            // does schema installation
                            schemaInstaller.InstallTableTracking(itemToInstall.TrackedTable);
                        }

                        // sets item status to "success" and updates UI
                        itemToInstall.InstallStatus = InstallStepStatus.Success;
                        _installSchemaBkgWorker.ReportProgress(100, new AsyncWorkProgressState(
                            AsyncWorkProgressType.StepItemStatus, new object[] { itemToInstall }));
                    }
                    catch (CdcSchemaInstallerCancelException)
                    {
                        // work cancelled when processing in schema installer class
                        // sets item status to "cancelled" and updates UI
                        itemToInstall.InstallStatus = InstallStepStatus.Cancelled;
                        _installSchemaBkgWorker.ReportProgress(0, new AsyncWorkProgressState(
                            AsyncWorkProgressType.StepItemStatus, new object[] { itemToInstall }));
                        return;
                    }
                    catch (Exception ex)
                    {
                        log.Error("Schema installation error.", ex);
                        // installation error
                        // sets item status to "failure", records exception and updates UI
                        itemToInstall.InstallationException = ex;
                        itemToInstall.InstallStatus = InstallStepStatus.Failure;
                        _installSchemaBkgWorker.ReportProgress(0, new AsyncWorkProgressState(
                            AsyncWorkProgressType.StepItemStatus, new object[] { itemToInstall }));
                    }

                    // checks if worker cancelled
                    if (_installSchemaBkgWorker.CancellationPending)
                    {
                        return;
                    }
                }

            }
            finally
            {
                // service must be set to initial state (finally block)
                try
                {
                    switch (initialServiceStatus)
                    {
                        case ServiceControllerStatus.Paused:
                            _agentServiceController.Start();
                            _agentServiceController.WaitForStatus(
                                ServiceControllerStatus.Running, AGENT_SERVICE_WAIT_TIME);
                            _agentServiceController.Pause();
                            break;
                        case ServiceControllerStatus.Running:
                            _agentServiceController.Start();
                            break;
                    }
                }
                catch (Exception ex)
                {
                    // stops installation, cannot do operations on the service
                    throw new NotOperableAgentServiceException("The agent service cannot be turned into the initial state," +
                        " which was before installation.", ex);
                }

                // changes service information icon
                _installSchemaBkgWorker.ReportProgress(0, new AsyncWorkProgressState(
                    AsyncWorkProgressType.ServiceStatus,
                    new object[] { GetAgentServiceIcon(initialServiceStatus) })
                    );
            }
        }

        /// <summary>
        /// Validates conditions required to perform schema installation.
        /// </summary>
        /// <param name="errorMessage">The list of failed conditions.</param>
        /// <returns>True if all required conditions are met, otherwise false.</returns>
        private bool IsReadyForSchemaInstallation(out string errorMessage)
        {
            bool isReady = true;
            errorMessage = "";

            if (_trackDbConnStr == null)
            {
                errorMessage += (errorMessage.Length > 0 ? Environment.NewLine : "") +
                    "No connection to tracked database.";
                isReady = false;
            }

            if (_stageDbConnStr == null)
            {
                errorMessage += (errorMessage.Length > 0 ? Environment.NewLine : "") +
                    "No connection to stage database.";
                isReady = false;
            }

            if (_storeDbConnStr == null)
            {
                errorMessage += (errorMessage.Length > 0 ? Environment.NewLine : "") +
                    "No connection to store database.";
                isReady = false;
            }

            if (_agentServiceController == null)
            {
                errorMessage += (errorMessage.Length > 0 ? Environment.NewLine : "") +
                    "No connection to agent service.";
                isReady = false;
            }

            if (!IsSchemaToInstall())
            {
                errorMessage += (errorMessage.Length > 0 ? Environment.NewLine : "") +
                    "No changes to install.";
                isReady = false;
            }

            return isReady;
        }


        /// <summary>
        /// Validates if in the list of items in the installation list, are any, which require an installation.
        /// </summary>
        /// <returns>True if found item to install, otherwise false.</returns>
        private bool IsSchemaToInstall()
        {
            foreach (ListViewInstallItem installItem in uxInstallationList.Items)
            {
                if (installItem.InstallStatus != InstallStepStatus.Success)
                {
                    return true;
                }
            }
            return false;
        }

        private bool IsSchemaUninstallOnly()
        {
            foreach (ListViewInstallItem installItem in uxInstallationList.Items)
            {
                if (!installItem.TrackedTable.IsDropTrackingRequested())
                {
                    return false;
                }
            }
            return true;
        }


        private void uxTrackedTablesTree_AfterCheck(object sender, TreeViewEventArgs e)
        {
            if (e.Action != TreeViewAction.Unknown)
            {
                if (e.Node is TreeNodeColumn)
                {
                    ((TreeNodeTable)e.Node.Parent).ColumnStateChanged((TreeNodeColumn)e.Node);
                }
                else
                {
                    ((TreeNodeTable)e.Node).StateChanged();
                }
            }
        }

        private void uxTrackedTablesTree_BeforeCheck(object sender, TreeViewCancelEventArgs e)
        {
            if (e.Action != TreeViewAction.Unknown)
            {
                if (e.Node is TreeNodeColumn)
                {
                    TreeNodeColumn colNode = (TreeNodeColumn)e.Node;
                    TreeNodeTable tblNode = (TreeNodeTable)e.Node.Parent;

                    e.Cancel = !tblNode.IsColumnStateChangeAllowed(colNode);
                }

                if (e.Node is TreeNodeTable)
                {
                    // in before check event, check box not changed yet, so filnal value will be negation
                    e.Cancel = !((TreeNodeTable)e.Node).IsTableStateChangeAllowed(!e.Node.Checked);
                }
            }
        }

        private void uxRefreshStop_Click(object sender, EventArgs e)
        {
            // checks if async operation in progress
            if (_refreshTableListBkgWorker != null && _refreshTableListBkgWorker.IsBusy)
            {
                _refreshTableListBkgWorker.CancelAsync();
            }
        }

        private void uxConnectServiceServer_Click(object sender, EventArgs e)
        {
            ServerConnection svrConn = new ServerConnection();

            if (_checkAgentServiceStatusBkgWorker != null && _checkAgentServiceStatusBkgWorker.IsBusy)
            {
                MessageBox.Show("Pending service checking.", "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                if (svrConn.ShowDialog() == DialogResult.OK)
                {
                    uxAgentServiceInfo.Text = svrConn.ServerName;
                    ConnectToAgentService(svrConn.ServerName);
                }
            }
        }

        private void ConnectToAgentService(string serverName)
        {
            if (string.IsNullOrEmpty(serverName))
            {
                throw new ArithmeticException("The serverName argument cannot be null or empty.");
            }

            uxAgentServiceStatusIcon.Image = uxImageList16.Images[ICON_AGENT_SERVICE_WAIT];

            // prepares and executes async service status check
            _checkAgentServiceStatusBkgWorker = new BackgroundWorker();
            _checkAgentServiceStatusBkgWorker.DoWork += CheckAgentService_DoWork;
            _checkAgentServiceStatusBkgWorker.RunWorkerCompleted += CheckAgentService_RunWorkerCompleted;
            _checkAgentServiceStatusBkgWorker.RunWorkerAsync(serverName);
        }

        private void CheckAgentService_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // sets service status icon according to service status
            if (e.Error != null)
            {
                // error occured, not possible to connect to service
                uxAgentServiceStatusIcon.Image = GetAgentServiceIcon(null);
            }
            else
            {
                // connection to service successful
                uxAgentServiceStatusIcon.Image = GetAgentServiceIcon(_agentServiceController.Status);
            }
        }

        /// <summary>
        /// Gets the image representing a service state passed in the argument.
        /// </summary>
        /// <param name="serviceStatus">The state of a service the icon is needed for.</param>
        /// <returns>The icon image of a service state.</returns>
        private Image GetAgentServiceIcon(ServiceControllerStatus? serviceStatus)
        {
            Image result;

            // validates input argument
            if (serviceStatus == null)
            {
                result = uxImageList16.Images[ICON_AGENT_SERVICE_ERROR];
            }
            else
            {
                switch (serviceStatus)
                {
                    case ServiceControllerStatus.Paused:
                        result = uxImageList16.Images[ICON_AGENT_SERVICE_PAUSE];
                        break;
                    case ServiceControllerStatus.Running:
                        result = uxImageList16.Images[ICON_AGENT_SERVICE_RUN];
                        break;
                    case ServiceControllerStatus.Stopped:
                        result = uxImageList16.Images[ICON_AGENT_SERVICE_STOP];
                        break;
                    default:
                        // drops here when service is in any of pending states
                        result = uxImageList16.Images[ICON_AGENT_SERVICE_ERROR];
                        break;
                }
            }

            return result;
        }

        private void CheckAgentService_DoWork(object sender, DoWorkEventArgs e)
        {
            _agentServiceController = new ServiceController(CommonHelper.CDC_AGENT_SERVICE_NAME, (string)e.Argument);

            // gets status to force service controller to pool status (can be time consuming)
            _agentServiceController.Refresh();
            ServiceControllerStatus dummy = _agentServiceController.Status;
        }

        /// <summary>
        /// Sets current application status indicators.
        /// </summary>
        /// <param name="status">The application status.</param>
        /// <param name="statusInfoExt">The extended information displayed under status icon.
        /// If <code>null</code> currently displayed text is left unchanged.</param>
        /// <param name="statusDetails">The status details displayed in message box which pops-up after
        /// clicking link label, displayed under application status text. If <code>null</code> currently
        /// displayed link lable left unchanged.</param>
        private void SetMainStatusInfo(MainStatus status, string statusInfoExt, string statusDetails)
        {
            switch (status)
            {
                case MainStatus.Ready:
                    uxMainStatus.Text = "Ready";
                    uxProgressCircle.Active = false;
                    uxProgressCircle.Visible = false;
                    uxMainStatusPicture.Visible = true;
                    uxMainStatusPicture.Image = uxImageList32.Images[ICON_MAIN_STATUS_READY];
                    break;
                case MainStatus.Processing:
                    uxMainStatus.Text = "Processing";
                    uxProgressCircle.Active = true;
                    uxProgressCircle.Visible = true;
                    uxMainStatusPicture.Visible = false;
                    break;
                case MainStatus.Cancelled:
                    uxMainStatus.Text = "Cancelled";
                    uxProgressCircle.Active = false;
                    uxProgressCircle.Visible = false;
                    uxMainStatusPicture.Visible = true;
                    uxMainStatusPicture.Image = uxImageList32.Images[ICON_MAIN_STATUS_FAILURE];
                    break;
                case MainStatus.Error:
                    uxMainStatus.Text = "Error";
                    uxProgressCircle.Active = false;
                    uxProgressCircle.Visible = false;
                    uxMainStatusPicture.Visible = true;
                    uxMainStatusPicture.Image = uxImageList32.Images[ICON_MAIN_STATUS_FAILURE];
                    break;
            }

            if (statusInfoExt != null)
            {
                uxMainStatusExt.Text = statusInfoExt;
            }

            if (statusDetails != null)
            {
                if (string.IsNullOrEmpty(statusDetails))
                {
                    uxMainStatusDetails.Visible = false;
                }
                else
                {
                    uxMainStatusDetails.Visible = true;
                    uxMainStatusDetails.Tag = statusDetails;
                }
            }
        }

        private void uxMainStatusDetails_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            MessageBox.Show((string)uxMainStatusDetails.Tag, "Status info details",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }


        private void AddTableToInstallList(SourceTable toAddTable)
        {
            if (toAddTable == null)
            {
                throw new ArgumentNullException("toAddTable");
            }

            uxInstallationList.Items.Add(new ListViewInstallItem(toAddTable));
            EnableDisableUIControls(UiContextState.SchemaToInstall);
        }

        private void RemoveTableFromInstallList(SourceTable toRemoveTable)
        {
            if (toRemoveTable == null)
            {
                throw new ArgumentNullException("toRemoveTable");
            }

            for (int idx = 0; idx < uxInstallationList.Items.Count; idx++)
            {
                if (((ListViewInstallItem)uxInstallationList.Items[idx]).TrackedTable.TableIdentifier ==
                    toRemoveTable.TableIdentifier)
                {
                    uxInstallationList.Items.RemoveAt(idx);
                    break;
                }
            }

            if (IsSchemaToInstall())
            {
                EnableDisableUIControls(UiContextState.SchemaToInstall);
            }
            else
            {
                EnableDisableUIControls(UiContextState.TablesLoaded);
            }
        }

        private void uxInstallationList_SelectedIndexChanged(object sender, EventArgs e)
        {
            // quit function if no item selected on the install items list
            if (uxInstallationList.SelectedItems.Count == 0) return;

            // collapses all table nodes
            uxTrackedTablesTree.CollapseAll();

            // expands a table node in the tables list control
            foreach (TreeNodeTable tblNode in uxTrackedTablesTree.Nodes)
            {
                if (tblNode.TableIdentifier == ((ListViewInstallItem)uxInstallationList.SelectedItems[0]).TableIdentifier)
                {
                    tblNode.Expand();
                }
            }
        }

        private void uxInstallStop_Click(object sender, EventArgs e)
        {
            _installSchemaBkgWorker.CancelAsync();

            // searches for item currently being installed, and changes its status to "Cancelling"
            foreach (ListViewInstallItem installItem in uxInstallationList.Items)
            {
                if (installItem.InstallStatus == InstallStepStatus.Running)
                {
                    installItem.InstallStatus = InstallStepStatus.Cancelling;
                    installItem.SetStepUiStatus();
                }
            }
        }

        private void uxInstallationList_MouseClick(object sender, MouseEventArgs e)
        {
            ListViewItem.ListViewSubItem subItem = uxInstallationList.HitTest(e.Location).SubItem;
            ListViewInstallItem item = ((ListViewInstallItem)uxInstallationList.HitTest(e.Location).Item);

            // displays error description if clicked status column and the item installation failed
            if (subItem.Name == "StepStatus" && item.InstallStatus == InstallStepStatus.Failure)
            {
                MessageBox.Show(item.InstallationException.Message, "Step install error", MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
            }

        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (IsSchemaToInstall() &&
                DialogResult.No == MessageBox.Show("All not installed changes will be lost. Continue?", "",
                MessageBoxButtons.YesNo, MessageBoxIcon.Question))
            {
                e.Cancel = true;
            }
        }

        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {
            SplashScreen splash = new SplashScreen(SplashType.Manager, int.MaxValue);
            splash.ShowDialog();
        }

        private void installAgentToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_trackDbConnStr == null || _stageDbConnStr == null || _storeDbConnStr == null)
            {
                MessageBox.Show("Connect to databases first.", "Db Connection required", MessageBoxButtons.OK,
                    MessageBoxIcon.Warning);
                return;
            }

            DialogResult res = MessageBox.Show("Agent Service will be configured. Continue?",
                "Service Configuration", MessageBoxButtons.YesNo, MessageBoxIcon.Question,
                MessageBoxDefaultButton.Button2);

            if (res == DialogResult.Yes)
            {
                ConfigureAgentService();
            }

        }

        private void ConfigureAgentService()
        {
            const string SERVICE_FILE = "CdcProcessorService.exe";
            string servicePath = Path.Combine(Application.StartupPath, SERVICE_FILE);
            string configPath = Path.Combine(Application.StartupPath, SERVICE_FILE + ".config");

            if (!File.Exists(configPath))
            {
                MessageBox.Show("Agent Service config file not found. " +
                    "Run setup program and install Agent Service component.",
                    "Config not found", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            Configuration srvConfig = ConfigurationManager.OpenExeConfiguration(servicePath);

            string connProvider = "System.Data.SqlClient";
            SqlConnectionStringBuilder trackB = new SqlConnectionStringBuilder(_trackDbConnStr.ConnectionString);
            SqlConnectionStringBuilder stageB = new SqlConnectionStringBuilder(_stageDbConnStr.ConnectionString);
            SqlConnectionStringBuilder storeB = new SqlConnectionStringBuilder(_storeDbConnStr.ConnectionString);

            trackB.ApplicationName = storeB.ApplicationName = stageB.ApplicationName = "CdcAgentService";
            trackB.AsynchronousProcessing = storeB.AsynchronousProcessing = stageB.AsynchronousProcessing = true;

            srvConfig.ConnectionStrings.ConnectionStrings.Clear();
            srvConfig.ConnectionStrings.ConnectionStrings.Add(new ConnectionStringSettings(
                "CdcTrackConnString", trackB.ConnectionString, connProvider));
            srvConfig.ConnectionStrings.ConnectionStrings.Add(new ConnectionStringSettings(
                "CdcStoreConnString", storeB.ConnectionString, connProvider));
            srvConfig.ConnectionStrings.ConnectionStrings.Add(new ConnectionStringSettings(
                "CdcStageConnString", stageB.ConnectionString, connProvider));

            try
            {
                srvConfig.Save();
            }
            catch
            {
                MessageBox.Show("Problem occured while saving connection string to Agent Service config file." +
                    Environment.NewLine + "Run TomCdc Manager as administrator and try again.",
                    "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return;
            }

            bool anyIntegratedSec = trackB.IntegratedSecurity || stageB.IntegratedSecurity || storeB.IntegratedSecurity;
            bool anySqlAerverSec = !trackB.IntegratedSecurity || !stageB.IntegratedSecurity || !storeB.IntegratedSecurity;

            ServiceController sc = new ServiceController(CommonHelper.CDC_AGENT_SERVICE_NAME);
            try
            {
                if (sc.Status == ServiceControllerStatus.Running)
                {
                    sc.Stop();
                    sc.WaitForStatus(ServiceControllerStatus.Stopped);
                }
                if (!anyIntegratedSec) sc.Start();
            }
            catch
            {
                MessageBox.Show("Problem occured while restarting Agent Service." +
                    Environment.NewLine + "Please restart Change Data Capture Service manually.",
                    "", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return;
            }

            if (anyIntegratedSec)
            {
                MessageBox.Show("One of database connections uses Integrated Security, " +
                    "configure Agent Service log on account and start service. Configured log on account needs access to " +
                    "database for which Integrated Security has been used.",
                    "", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }

            if (anySqlAerverSec)
            {
                MessageBox.Show("One of databse connections uses Sql Server Authentication, " +
                    "user name and password has been saved to Agent Service config file in a plain text. " +
                    " If that's against your security policy, modfiy config file to use Integrated Security " +
                    "and configure Agent Service log on account. Configured log on account needs access to " +
                    "database for which Integrated Security has been used.",
                    "", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

    }
}
